﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/Direction.cs
//  *
//  ***************************************************************************

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

using Haplous.Core;
using Haplous.Correctness;

using JetBrains.Annotations;

namespace Haplous
{
    /// <summary>
    ///     A simple enumeration of directions.
    /// </summary>
    public enum DirectionKind
    {
        /// <summary>
        ///     There is no direction of travel.
        /// </summary>
        None = 0,

        /// <summary>
        ///     Moving up.
        /// </summary>
        Up,

        /// <summary>
        ///     Moving down.
        /// </summary>
        Down,

        /// <summary>
        ///     Moving left.
        /// </summary>
        Left,

        /// <summary>
        ///     Moving right.
        /// </summary>
        Right
    }

    /// <summary>
    ///     A class that allows a coarse direction to be defined and controlled. This class cannot be inherited.
    /// </summary>
    [DebuggerDisplay("Direction: {Value}")]
    public sealed class Direction : NotifyObject
    {
        /// <summary>
        ///     The current direction of travel.
        /// </summary>
        private DirectionKind _value;

        /// <summary>
        ///     Initializes a new instance of the <see cref="T:Direction" /> class.
        /// </summary>
        /// <remarks>The initial direction of travel is <see cref="DirectionKind.None" />.</remarks>
        public Direction()
        {}

        /// <summary>
        ///     Initializes a new instance of the <see cref="T:Direction" /> class.
        /// </summary>
        /// <param name="value">The initial direction of travel.</param>
        public Direction(DirectionKind value)
        {
            _value = value;
        }

        /// <summary>
        ///     The current direction of travel.
        /// </summary>
        public DirectionKind Value
        {
            get { return _value; }
            set { OnPropertyChanged("Value", ref _value, value); }
        }

        /// <summary>
        ///     Gets a value indicating whether the current direction of travel is <see cref="DirectionKind.Left" />.
        /// </summary>
        /// <value>
        ///     <see langword="true" /> if the current direction of travel is <see cref="DirectionKind.Left" />; otherwise,
        ///     <see langword="false" />.
        /// </value>
        public bool IsLeft
        {
            get { return _value == DirectionKind.Left; }
        }

        /// <summary>
        ///     Gets a value indicating whether the current direction of travel is <see cref="DirectionKind.Right" />.
        /// </summary>
        /// <value>
        ///     <see langword="true" /> if the current direction of travel is <see cref="DirectionKind.Right" />; otherwise,
        ///     <see langword="false" />.
        /// </value>
        public bool IsRight
        {
            get { return _value == DirectionKind.Right; }
        }

        /// <summary>
        ///     Gets a value indicating whether the current direction of travel is <see cref="DirectionKind.Up" />.
        /// </summary>
        /// <value>
        ///     <see langword="true" /> if the current direction of travel is <see cref="DirectionKind.Up" />; otherwise,
        ///     <see langword="false" />.
        /// </value>
        public bool IsUp
        {
            get { return _value == DirectionKind.Up; }
        }

        /// <summary>
        ///     Gets a value indicating whether the current direction of travel is <see cref="DirectionKind.Down" />.
        /// </summary>
        /// <value>
        ///     <see langword="true" /> if the current direction of travel is <see cref="DirectionKind.Down" />; otherwise,
        ///     <see langword="false" />.
        /// </value>
        public bool IsDown
        {
            get { return _value == DirectionKind.Down; }
        }

        /// <summary>
        ///     Gets a value indicating whether the current direction of travel is <see cref="DirectionKind.None" />.
        /// </summary>
        /// <value>
        ///     <see langword="true" /> if the current direction of travel is <see cref="DirectionKind.None" />; otherwise,
        ///     <see langword="false" />.
        /// </value>
        public bool IsNone
        {
            get { return _value == DirectionKind.None; }
        }

        /// <summary>
        ///     Gets a value indicating whether the current direction of travel is horizontal (<see cref="DirectionKind.Up" /> or
        ///     <see cref="DirectionKind.Down" />).
        /// </summary>
        /// <value>
        ///     <see langword="true" /> if the current direction of travel is (<see cref="DirectionKind.Up" /> or
        ///     <see cref="DirectionKind.Down" />); otherwise, <see langword="false" />.
        /// </value>
        public bool IsVertical
        {
            get
            {
                switch(_value)
                {
                    case DirectionKind.Up:
                        return true;

                    case DirectionKind.Down:
                        return true;

                    default:
                        return false;
                }
            }
        }

        /// <summary>
        ///     Gets a value indicating whether the current direction of travel is horizontal (<see cref="DirectionKind.Left" /> or
        ///     <see cref="DirectionKind.Right" />).
        /// </summary>
        /// <value>
        ///     <see langword="true" /> if the current direction of travel is (<see cref="DirectionKind.Left" /> or
        ///     <see cref="DirectionKind.Right" />); otherwise, <see langword="false" />.
        /// </value>
        public bool IsHorizontal
        {
            get
            {
                switch(_value)
                {
                    case DirectionKind.Left:
                        return true;

                    case DirectionKind.Right:
                        return true;

                    default:
                        return false;
                }
            }
        }

        /// <summary>
        ///     Rotates the direction of travel 90 degrees clockwise.
        /// </summary>
        /// <returns>The resulting direction.</returns>
        /// <remarks>A direction of <see cref="DirectionKind.None" /> cannot be rotated and will remain unchanged.</remarks>
        public DirectionKind RotateClockwise()
        {
            switch(_value)
            {
                case DirectionKind.None:
                    break;
                case DirectionKind.Up:
                    Value = DirectionKind.Right;
                    break;
                case DirectionKind.Down:
                    Value = DirectionKind.Left;
                    break;
                case DirectionKind.Left:
                    Value = DirectionKind.Up;
                    break;
                case DirectionKind.Right:
                    Value = DirectionKind.Down;
                    break;
            }

            return _value;
        }

        /// <summary>
        ///     Rotates to the specified direction.
        /// </summary>
        /// <param name="direction">
        ///     The direction, relative to the direction of travel, to which we should always keep a wall.
        ///     Must be either <see cref="DirectionKind.Left" /> or <see cref="DirectionKind.Right" />.
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     Only <see cref="DirectionKind.Left" /> or
        ///     <see cref="DirectionKind.Right" /> are valid values of the <paramref name="direction" /> argument.
        /// </exception>
        public void Rotate(DirectionKind direction)
        {
            switch(direction)
            {
                case DirectionKind.Left:
                    RotateCounterclockwise();
                    break;
                case DirectionKind.Right:
                    RotateClockwise();
                    break;
                default:
                    throw new ArgumentOutOfRangeException("direction", "Only Left and Right are valid values of the 'direction' argument.");
            }
        }

        /// <summary>
        ///     Rotates the direction of travel 90 degrees counter-clockwise.
        /// </summary>
        /// <returns>The resulting direction.</returns>
        /// <remarks>A direction of <see cref="DirectionKind.None" /> cannot be rotated and will remain unchanged.</remarks>
        public DirectionKind RotateCounterclockwise()
        {
            switch(_value)
            {
                case DirectionKind.None:
                    break;
                case DirectionKind.Up:
                    Value = DirectionKind.Left;
                    break;
                case DirectionKind.Down:
                    Value = DirectionKind.Right;
                    break;
                case DirectionKind.Left:
                    Value = DirectionKind.Down;
                    break;
                case DirectionKind.Right:
                    Value = DirectionKind.Up;
                    break;
            }

            return _value;
        }

        /// <summary>
        ///     Translates the specified offset from the relative coordinates of the direction to absolute coordinates.
        /// </summary>
        /// <param name="offset">The offset.</param>
        /// <returns>A new point specifying absolute coordinates.</returns>
        /// <exception cref="System.ArgumentOutOfRangeException">offset</exception>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        [NotNull]
        public PositionInt32 Translate([NotNull] PositionInt32 offset)
        {
            offset.ValidateArgument("offset");

            switch(_value)
            {
                case DirectionKind.Up:
                    return new PositionInt32(offset.X, -offset.Y);

                case DirectionKind.Down:
                    return new PositionInt32(offset.X, offset.Y);

                case DirectionKind.Left:
                    return new PositionInt32(-offset.Y, offset.X);

                case DirectionKind.Right:
                    return new PositionInt32(offset.Y, -offset.X);

                default:
                    throw new ArgumentOutOfRangeException("offset", offset, "");
            }
        }

        /// <summary>
        ///     Returns the solid, if any, found at the coordinates given.
        /// </summary>
        /// <param name="game">The game in which to perform the hit test</param>
        /// <param name="anchor">The fixed position from which to calculate the relative position.</param>
        /// <param name="offset">The offset to add to the fixed position in the relative direction.</param>
        /// <param name="relativeDirection">The relative direction.</param>
        /// <param name="size">The size of the area to check.</param>
        /// <returns>A <see cref="SolidGraphicBlock" />An enumerable containing all solids founds in the area specified.</returns>
        [NotNull]
        public IEnumerable<SolidGraphicBlock> HitTest([NotNull] Game game, [NotNull] PositionInt32 anchor, [NotNull] PositionInt32 offset, DirectionKind relativeDirection, [NotNull] SizeInt32 size)
        {
            anchor.ValidateArgument("anchor");
            offset.ValidateArgument("offset");
            size.ValidateArgument("size");
            game.ValidateArgument("game");

            PositionInt32 position;

            switch(Translate(relativeDirection))
            {
                case DirectionKind.None:
                    position = anchor.Clone();
                    break;
                case DirectionKind.Up:
                    position = new PositionInt32(anchor.X + offset.X, anchor.Y - offset.Y);
                    break;
                case DirectionKind.Down:
                    position = new PositionInt32(anchor.X - offset.X, anchor.Y + offset.Y);
                    break;
                case DirectionKind.Left:
                    position = new PositionInt32(anchor.X - offset.Y, anchor.Y + offset.X);
                    break;
                case DirectionKind.Right:
                    position = new PositionInt32(anchor.X + offset.Y, anchor.Y - offset.X);
                    break;
                default:
                    throw new ArgumentOutOfRangeException("Translate(relativeDirection)"); // This should never happen!
            }

            foreach(var layer in game.Layers)
            {
                Debug.Assert(layer != null, "layer != null");
                foreach(var element in layer.Elements)
                {
                    var solid = element as SolidGraphicBlock;
                    if(solid != null)
                    {
                        if(position.IsOverlapping(size, solid.Position, solid.Size, position))
                            yield return solid;
                    }
                }
            }
        }

        /// <summary>
        ///     Translates a direction to be relative to the current direction of travel.
        /// </summary>
        /// <param name="relativeDirection">The relative direction.</param>
        /// <returns>An absolute direction relative to the game.</returns>
        public DirectionKind Translate(DirectionKind relativeDirection)
        {
            return Translate(_value, relativeDirection);
        }

        /// <summary>
        ///     Translates a direction relative to the direction of travel in an absolute direction.
        /// </summary>
        /// <param name="directionOfTravel">The direction of travel.</param>
        /// <param name="relativeDirection">The relative direction.</param>
        /// <returns>An absolute direction relative to the game.</returns>
        public static DirectionKind Translate(DirectionKind directionOfTravel, DirectionKind relativeDirection)
        {
            switch(directionOfTravel)
            {
                case DirectionKind.None:
                    return DirectionKind.None;

                case DirectionKind.Up:
                    return relativeDirection;

                case DirectionKind.Down:
                    switch(relativeDirection)
                    {
                        case DirectionKind.None:
                            return DirectionKind.None;
                        case DirectionKind.Up:
                            return DirectionKind.Down;
                        case DirectionKind.Down:
                            return DirectionKind.Up;
                        case DirectionKind.Left:
                            return DirectionKind.Right;
                        case DirectionKind.Right:
                            return DirectionKind.Left;
                        default:
                            throw new ArgumentOutOfRangeException("relativeDirection", relativeDirection, "The value supplied in 'relativeDirection' is not a valid enumeration.");
                    }
                case DirectionKind.Left:
                    switch(relativeDirection)
                    {
                        case DirectionKind.None:
                            return DirectionKind.None;
                        case DirectionKind.Up:
                            return DirectionKind.Left;
                        case DirectionKind.Down:
                            return DirectionKind.Right;
                        case DirectionKind.Left:
                            return DirectionKind.Down;
                        case DirectionKind.Right:
                            return DirectionKind.Up;
                        default:
                            throw new ArgumentOutOfRangeException("relativeDirection", relativeDirection, "The value supplied in 'relativeDirection' is not a valid enumeration.");
                    }
                case DirectionKind.Right:
                    switch(relativeDirection)
                    {
                        case DirectionKind.None:
                            return DirectionKind.None;
                        case DirectionKind.Up:
                            return DirectionKind.Right;
                        case DirectionKind.Down:
                            return DirectionKind.Left;
                        case DirectionKind.Left:
                            return DirectionKind.Up;
                        case DirectionKind.Right:
                            return DirectionKind.Down;
                        default:
                            throw new ArgumentOutOfRangeException("relativeDirection", relativeDirection, "The value supplied in 'relativeDirection' is not a valid enumeration.");
                    }
                default:
                    throw new ArgumentOutOfRangeException("directionOfTravel", directionOfTravel, "The value supplied in 'directionOfTravel' is not a valid enumeration.");
            }
        }
    }
}